package model;

import java.util.Iterator;
import java.util.LinkedList;
import java.util.TreeSet;

import main.Simulator;
import event.ACKReceivedEvent;
import event.Event;

/**
 * Classe que representa a entidade receptora do protocolo TCP.
 */
public class RxTCP {

	Simulator simulator;
	private int sessionId;
	private double propagationTime;
	int MSS;
	private long packetsReceiveds = 0;
	
	// Conjunto ordenado de intervalos de bytes recebidos pelo receptor.
	TreeSet<Pair> bytesInterval = new TreeSet<Pair>();
	
	public RxTCP(Simulator simulator, int sessionId, double propagationTime) {
		this.simulator = simulator;
		this.sessionId = sessionId;
		this.propagationTime = propagationTime;
		this.MSS = simulator.getProperties().getMSS();
		
		// Inicia o conjunto com o intervalo [-MSS, -MSS] para garantir a lógica correta do envio de ACKs.
		bytesInterval.add(new Pair(-MSS, -MSS));
	}
	
	/**
	 * Método do receptor para processar o recebimento de um pacote IP.
	 * O receptor ao final programa o evento de recebimento do ACK no transmissor.
	 * @param pack Pacote IP recebido pelo receptor.
	 */
	public ACK receivePacket(Packet pack) {
		if (pack.getSessionId() != sessionId) {
			throw new IllegalStateException("Recebendo pacote de sessão diferente.");
		}
		
		setPacketsReceiveds(getPacketsReceiveds() + 1);
		
		// Adiciona o pacote recebido no conjunto de pacotes apenas se o byte é maior que o primeiro do conjunto.
		long packByte = pack.getFirstByte();
		if (packByte > bytesInterval.first().getSecond()) {
			boolean added = false;
			Iterator<Pair> iterator = bytesInterval.iterator();
			while (iterator.hasNext()) {
				Pair item = iterator.next();
				
				// Se o byte estiver dentro do intervalo, não adiciona.
				if (packByte >= item.getFirst() && packByte <= item.getSecond()) {
					added = true;
					break;
				}
				
				// Se o byte é o início do intervalo menos MSS, então o início do intervalo passa a ser packByte.
				if (packByte == item.getFirst() - MSS) {
					item.setFirst(packByte);
					added = true;
					break;
				}
				
				// Se o byte é o fim do intervalo mais MSS, então o fim do intervalo passa a ser packByte.
				if (packByte == item.getSecond() + MSS) {
					item.setSecond(packByte);
					if (iterator.hasNext()) {
						Pair nextItem = iterator.next();
						// Se o próximo intervalo é seguido do atual, então juntamos os intervalos.
						if (item.getSecond() + MSS == nextItem.getFirst()) {
							item.setSecond(nextItem.getSecond());
							bytesInterval.remove(nextItem);
						}
					}
					added = true;
					break;
				}
			}
			
			// Se o byte não estiver no meio de nenhum intervalo e nem nas extremidades, então é adicionado o intervalo com [packByte, packByte].
			if (!added) {
				bytesInterval.add(new Pair(packByte, packByte));
			}
		}
		
		// Analisa a lista de intervalos de bytes e adiciona na lista encadeada quais bytes estão faltando.
		LinkedList<Long> missingBytes = new LinkedList<Long>();
		if (bytesInterval.size() == 1) {
			missingBytes.add(bytesInterval.first().getSecond() + MSS);
		} else {
			Iterator<Pair> iterator = bytesInterval.iterator();
			Pair item1 = iterator.next();
			while (iterator.hasNext()) {
				Pair item2 = iterator.next();
				for (long missedByte = item1.getSecond() + MSS; missedByte < item2.getFirst(); missedByte += MSS) {
					missingBytes.add(missedByte);
				}
				item1 = item2;
			}
		}
		
		// Cria o evento de recebimento de ACK no transmissor.
		double time = simulator.getTime() + propagationTime;
		ACK ack = new ACK(missingBytes, pack);
		Event event = new ACKReceivedEvent(time, ack);
		simulator.addEvent(event);
		return ack;
	}
	
	public long getPacketsReceiveds() {
		return packetsReceiveds;
	}

	public void setPacketsReceiveds(long packetsReceiveds) {
		this.packetsReceiveds = packetsReceiveds;
	}

	class Pair implements Comparable<Pair>{
		
		private long first;
		private long second;

		public Pair(long first, long second) {
			this.setFirst(first);
			this.setSecond(second);
		}

		public long getFirst() {
			return first;
		}

		public void setFirst(long first) {
			this.first = first;
		}

		public long getSecond() {
			return second;
		}

		public void setSecond(long second) {
			this.second = second;
		}

		@Override
		public int compareTo(Pair o) {
			return Long.compare(first, o.first) != 0 ? Long.compare(first, o.first): Long.compare(second, o.second); 
		}
		
		@Override
		public String toString() {
			return "["+first+", "+second+"]";
		}
	}
	
}
